"""
Enrol module.

Utility functions:
    readlines(filename)
    readtable(filename)
    writelines(filename)

Public interface:
    subjects()
    subjectName(subjectCode)
    classes()
    classInfo(classID)
    checkStudent(studentID, [subjectCode])
    enrol(studentID, classCode)
"""
import os
import glob

def readlines(filename):
    """Returns a list of all the lines in a file, 
    except for those starting with a #. 
    filename is a string containing the name of a file."""
    lines = []
    for line in open(filename):
        if line[0] == '#' or len(line) <= 0:
            continue
        line = line.rstrip('\n')
        lines.append(line)
    return lines

def readtable(filename):
    """Reads a file of colon-delimited lines and returns 
    a list of lists, with each line being represented as 
    a list of strings, split on colons.
    """
    table = []
    for line in open(filename):
        if line[0] == '#' or len(line) <= 0:
            continue
        line = line.rstrip('\n').split(':')
        table.append(line)
    return table

def writelines(filename, lines):
    """
    This function writes a list of strings safely to 
    the file filename, replacing any content already there. 
    By 'safely', it should write the lines to a file with a 
    different name in the directory, and if successful, 
    deletes the file named in filename, renames the new 
    file to its name and returns the value 1. 
    If an error occurs during writing 
    (and an exception is raised), it deletes the new 
    file and returns 0.
    """
    tempFileName = filename+".tmp"
    f = open(tempFileName, "w")
    ret = 0
    try:
        for line in lines:
            f.write(line+"\n")

        if len(lines) == 0:
            f.write("")
        ret = 1
    finally:
        f.close()
        if ret == 1:
            os.remove(filename)
            os.rename(tempFileName, filename)
        return ret
 

def _loadSubjects(enrolObj):
    """Load Subjects into memory
       Subjects {'subjectCode':'subjectName'...}
    """
    print"Load Subjects..."
    for line in readtable(enrolObj.filePath+"SUBJECTS"):
        if(len(line) < 2): #skip the lines has less than 2 fields
            continue
        enrolObj.Subjects[line[0]] = line[1]

def _loadClasses(enrolObj):
    """
    Load Classes into memory
    Classes{'ClassID':'_Class(object)'}
    """
    print "Load Classes..."
    for line in readtable(enrolObj.filePath+"CLASSES"):
        if(len(line) < 5): #skip the lines has less than 5 fields
            continue
        enrolObj.Classes[line[0]] = _Class(line[0],line[1],line[2],line[3],line[4])    

def _loadVenues(enrolObj):
    """Load Venues into memory
        Venues{'Venue':'capacity(int)'}
    """
    print "Load Venues..."
    for line in readtable(enrolObj.filePath+"VENUES"):
        if(len(line) < 2): #skip the lines has less than 2 fields
            continue
        enrolObj.Venues[line[0]] = int(line[1])

def _loadEnroment(enrolObj):
    """
    Load all enrolment, load all files end with .roll
    """
    print "Load Enrolment..."
    enrolFile = glob.glob(enrolObj.filePath+"*.roll")
    for fileName in enrolFile:
        classID = fileName.rstrip(".roll").lstrip(enrolObj.filePath)
        if classID in enrolObj.Classes:
            venueCap = enrolObj.Venues[enrolObj.Classes[classID].Venue]
            for studentID in readlines(fileName):
                if len(studentID) <= 0:
                    continue
                ret = enrolObj.Classes[classID].AddStudent(studentID, venueCap)
                if ret == 0:
                    print "Class[%s]->Student[%s]--[OK]"%(classID, studentID)
                elif ret == 1:
                    print "Class[%s]->Student[%s]--[Already Exist! Duplicate Data, please Check!]"%(classID, studentID)
                elif ret == 2:
                    print "Class[%s]->Student[%s]--[Class FULL. Student not added]"
        else:
            print "Unknown Class[%s] having students. Please Check!"%classID

class _Class(object):
    """
    The internal data structure that holds information about
    a particular class, like class ID, subject code, time, venues
    tutor and current student enroled
    """
    def __init__(self, classID, subjectCode, time, venue, tutor):
        """
        Init static information from CLASSES file, also init an empty
        list to store student
        """
        self.Students = []
        self.ClassID = classID
        self.SubjectCode = subjectCode
        self.Time = time
        self.Venue = venue
        self.Tutor = tutor

    def AddStudent(self, studentID, venueCap):
        """
        Add a student into current class.
        Check if the student is already in the class.
        Check if current venue reachs it's max capacity
        return 0 for successful add
        """
        if studentID in self.Students:
            return 1 #Student already exist
        if len(self.Students) >= venueCap:
            return 2 #Reached max capacity of the class
        self.Students.append(studentID)
        return 0

    def RemoveStudent(self, studentID):
        """
        Remove a student from current class
        return studentID if removed
        return None if student list un-changed
        """
        if studentID not in self.Students:
            return 0
        
        self.Students.remove(studentID)
        return 1

class Enrol(object):
    """
    The Class handles enroment.
    """
    def __init__(self, ENROLDIR = None):

        """
        The constructor. Accepts one argument: the name of the 
        directory where the enrolment data is kept. When the object 
        is constructed, it should read its data from the directory
        """
        print"Enrolment System Initializing...."
        self.Subjects = {}
        self.Classes = {}
        self.Venues= {}
        bPathFound = False
        if ENROLDIR != None and len(ENROLDIR)>0 and os.path.isdir(ENROLDIR):
            self.filePath = ENROLDIR
            bPathFound = True
 
        if not bPathFound:
           print"ENROLDIR not found, search from current dir"
           if os.name == "nt":
               self.filePath = ".\data\\"
           else:
               self.filePath = "./data/"
      
        _loadSubjects(self)
        _loadClasses(self)
        _loadVenues(self)
        _loadEnroment(self)

    def subjects(self):
        """Returns a list of subject codes handled by the 
        enrolment system. Accepts no arguments. """
        return self.Subjects.keys()

    def subjectName(self, subjectCode = None):
        """Returns a list of class IDs for a particular subject. 
        Accepts one argument: the subject code of a subject. 
        Raise KeyError if the subject does not exist. """
        if subjectCode == None:
            return None
        try:
            subjectName = self.Subjects[subjectCode]
        except KeyError:
            return None
        else:
            return subjectName

    def classes(self, subjectCode):
        """
        Returns a list of class IDs for a particular subject. 
        Accepts one argument: the subject code of a subject. 
        Raise KeyError if the subject does not exist.
        """
        if subjectCode not in self.Subjects:
            raise KeyError("Subject ["+subjectCode+"] not found")

        ret = []
        for _class in self.Classes.values():
            if _class.SubjectCode == subjectCode:
                ret.append(_class.ClassID)

        return ret

    def classInfo(self, classID):
        """
        Returns information about a class. Accepts one argument: 
        a string containing the class ID. Returns a tuple of the 
        form (subjcode, time, room, tutor, students). The first four elements 
        are strings, and contain the information as described in the CLASSES 
        file specification above. The last item is a list of the student IDs 
        enrolled in the class. Raise KeyError if the class does not exist.
        """
        if classID not in self.Classes:
            raise KeyError("Class ["+classID+"] not found")
        val = self.Classes[classID]

        return (val.SubjectCode, val.Time, val.Venue, val.Tutor, val.Students)

    def checkStudent(self, studentID, subjectCode = None):
        """
        Checks which classes a student is enrolled in. Accepts one or two arguments. 
        The first (required) argument is the student ID to check. The second (optional) 
        argument is an optional subject code. If a subject code is specified, 
        returns the class code of the class in which the student is enrolled for that 
        subject; if the student isn't enrolled in any class in that subject, it returns 
        None. If no subject code is specified, it returns a list of zero or more 
        class codes the student is enrolled in across all possible subjects.
        """
        if subjectCode is not None:
            try:
                classList = self.classes(subjectCode)
                if len(classList) == 0:
                    return None
                for classID in classList:
                    if studentID in self.Classes[classID].Students:
                        return classID
                return None
            except KeyError:
                return None
        else:
            ret = []
            for subjectCode in self.Subjects:
                val = self.checkStudent(studentID, subjectCode)
                if val is not None:
                    ret.append(val)
            return ret

    def enrol(self, studentID, classID):
        """
        Attempts to enrol a student in a class. Accepts two arguments: a student ID 
        and a class code. It returns 1 if successful, None if not. 
        Before attempting to enrol a student in a class, it attempts to check whether 
        the number of students in the class is less than the capacity of the class's venue. 
        If not, then there is no space in the class and it fails. If there is space, it proceeds. 
        If the student is enrolled in any other classes in the same subject as the class, 
        she is removed from those classes and placed in the new one. 
        Raise KeyError if the class does not exist.
        """
        if classID not in self.Classes:
            raise KeyError

        if studentID in self.Classes[classID].Students: # Check if the student already in the class
            return None

        classToRemove = self.checkStudent(studentID, self.Classes[classID].SubjectCode)
        venueCap = self.Venues[self.Classes[classID].Venue]
        ret = self.Classes[classID].AddStudent(studentID, venueCap)
        if ret == 0:
            filePath = self.filePath+classID+".roll"
            writelines(filePath, self.Classes[classID].Students)
            #Try to remove student from a class under this subject
            if classToRemove is not None:
                self.Classes[classToRemove].RemoveStudent(studentID)
                filePath = self.filePath+classToRemove+".roll"
                writelines(filePath, self.Classes[classToRemove].Students)  
            return 1
        else:
            return None